package com.itheima.search.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.itheima.search.domain.HotNewsVO;
import com.itheima.search.domain.NewsVO;
import com.itheima.search.domain.SearchParam;
import com.itheima.search.domain.SearchResult;
import com.itheima.search.service.ISearchService;
import com.itheima.search.utils.TextUtils;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentFactory;
import org.redisson.api.RScoredSortedSet;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.redisson.client.protocol.ScoredEntry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.*;

/**
 * 搜索服务实现类
 * 提供基于 Elasticsearch 的新闻搜索功能
 */
@Service
public class SearchServiceImpl implements ISearchService {

    /**
     * 用于与 Elasticsearch 进行交互的客户端（已过时，建议使用新的 Java High Level REST Client 或 Elasticsearch Java API）
     */
    @Autowired
    private RestHighLevelClient restHighLevelClient;

    /**
     * Redisson 客户端，可用于缓存热搜词等信息（当前未在搜索中直接使用）
     */
    @Autowired
    private RedissonClient redissonClient;

    // 热搜榜单 Redis Key
    private static final String HOT_NEWS_KEY = "hot_news_set";

    // Elasticsearch 中新闻索引名称
    private static final String INDEX_NAME = "news_index";

    /**
     * 新闻搜索接口实现
     *
     * @param param 搜索参数，包含关键词、分页信息等
     * @return 包含搜索结果及分页信息的对象
     * @throws IOException 如果 ES 查询失败或解析异常
     */
    @Override
    public SearchResult searchNews(SearchParam param) throws IOException {
        // 初始化一个空的结果对象
        SearchResult result = SearchResult.empty();

        try {
            // 构建查询条件
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

            // 如果有关键词，则构建多字段匹配查询：title 和 content
            if (StrUtil.isNotBlank(param.getKeyword())) {
                MultiMatchQueryBuilder multiMatchQuery = QueryBuilders.multiMatchQuery(
                        param.getKeyword(), "title", "content"
                );
                boolQuery.must(multiMatchQuery);
            }

            // 设置查询主语句
            sourceBuilder.query(boolQuery);

            // 高亮设置（仅针对 title 和 content 字段）
            if (StrUtil.isNotBlank(param.getKeyword())) {
                HighlightBuilder highlightBuilder = new HighlightBuilder();
                // 对 title 字段进行高亮
                highlightBuilder.field("title")
                        .preTags("<em>")  // 高亮前缀标签
                        .postTags("</em>"); // 高亮后缀标签
                // 对 content 字段进行高亮
                highlightBuilder.field("content")
                        .preTags("<em>")
                        .postTags("</em>");
                sourceBuilder.highlighter(highlightBuilder); // 将高亮器加入查询
            }

            // 分页设置
            int pageNo = param.getPageNo();       // 当前页码
            int pageSize = param.getPageSize();   // 每页数量
            sourceBuilder.from((pageNo - 1) * pageSize).size(pageSize); // 计算起始位置和大小

            // 排序设置（当前被注释掉，如需按时间排序可启用）
            //sourceBuilder.sort("createTime", SortOrder.DESC);

            // 构建最终的搜索请求
            SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
            searchRequest.source(sourceBuilder);

            // 执行搜索
            SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

            // 解析返回结果
            long total = response.getHits().getTotalHits().value; // 总命中数
            result.setTotal(total); // 设置总条数

            // 计算总页数
            long pages = total % pageSize == 0 ? total / pageSize : total / pageSize + 1;
            result.setPages(pages); // 设置总页数

            List<NewsVO> newsList = new ArrayList<>(); // 存储每条新闻数据

            for (SearchHit hit : response.getHits()) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap(); // 获取原始数据 map

                NewsVO news = new NewsVO(); // 创建 VO 对象

                // 处理 newsId 字段，兼容 Long/String 类型
                Object newsIdObj = sourceAsMap.get("newsId");
                if (newsIdObj != null) {
                    if (newsIdObj instanceof Number) {
                        news.setNewsId(((Number) newsIdObj).longValue());
                    } else if (newsIdObj instanceof String) {
                        try {
                            news.setNewsId(Long.parseLong((String) newsIdObj));
                        } catch (NumberFormatException ignored) {
                            // 忽略无法转换的值
                        }
                    }
                }

                // 设置标题、来源、类型ID、新闻类型等基础字段
                news.setTitle((String) sourceAsMap.get("title"));
                news.setSource((String) sourceAsMap.get("source"));
                news.setTypeId((String) sourceAsMap.get("typeId"));
                news.setNewsType((String) sourceAsMap.get("newsType"));

                // 设置内容，并调用工具类提取纯文本（截取前100字）
                news.setContent((String) sourceAsMap.get("content"));
                news.setContent(TextUtils.extractPlainText(news.getContent(), 100));

                // 处理 status 字段，兼容 Integer/String 类型
                Object statusObj = sourceAsMap.get("status");
                if (statusObj != null) {
                    if (statusObj instanceof Integer) {
                        news.setStatus((Integer) statusObj);
                    } else {
                        try {
                            news.setStatus(Integer.parseInt(statusObj.toString()));
                        } catch (NumberFormatException ignored) {}
                    }
                }

                // 处理 createTime 字段，Elasticsearch 返回的是字符串格式
                String createTimeStr = (String) sourceAsMap.get("createTime");
                if (createTimeStr != null) {
                    try {
                        news.setCreateTime(DateUtil.parse(createTimeStr)); // 转换为 Date 类型
                    } catch (Exception ignored) {}
                }

                // 替换高亮后的标题和内容
                Map<String, HighlightField> highlightFields = hit.getHighlightFields();
                if (highlightFields != null) {
                    if (highlightFields.containsKey("title")) {
                        news.setTitle(highlightFields.get("title").fragments()[0].string());
                    }
                    if (highlightFields.containsKey("content")) {
                        news.setContent(highlightFields.get("content").fragments()[0].string());
                    }
                }

                newsList.add(news); // 添加到结果列表
            }

            result.setRecords(newsList); // 设置记录集合

        } catch (IOException | ElasticsearchException e) {
            throw new RuntimeException("查询ES中新闻失败！", e);
        }

        return result;
    }

    /**
     * 根据新闻ID查询新闻详情
     *
     * 从 Elasticsearch 中根据 newsId 查询新闻数据，封装为 NewsVO 对象返回。
     * 支持字段类型兼容处理（如 String 转 Long）、高亮内容替换，
     * 并在成功获取后调用 clickNews 增加新闻热度。
     *
     * @param newsId 新闻唯一标识符（Long 类型）
     * @return NewsVO 新闻详情对象，若未找到或出错则返回 null
     */
    @Override
    public NewsVO searchNewsById(Long newsId) {
        // 参数校验：newsId 不可为空
        if (newsId == null) {
            return null;
        }

        try {
            // 构建搜索源构建器
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

            // 构建布尔查询：精确匹配 newsId 字段
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
                    .must(QueryBuilders.termQuery("newsId", newsId));

            // 设置查询条件到源构建器中
            sourceBuilder.query(boolQuery);

            // 构建最终的搜索请求
            SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
            searchRequest.source(sourceBuilder);

            // 执行搜索操作
            SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

            // 解析响应结果
            if (response.getHits().getTotalHits().value > 0) {
                // 取第一条命中结果
                SearchHit hit = response.getHits().getAt(0);

                // 获取原始数据 map 形式
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();

                // 创建 VO 对象用于封装数据
                NewsVO news = new NewsVO();

                // 处理 newsId 字段，兼容 Number 和 String 类型
                Object newsIdObj = sourceAsMap.get("newsId");
                if (newsIdObj != null) {
                    if (newsIdObj instanceof Number) {
                        news.setNewsId(((Number) newsIdObj).longValue());
                    } else if (newsIdObj instanceof String) {
                        try {
                            news.setNewsId(Long.parseLong((String) newsIdObj));
                        } catch (NumberFormatException ignored) {
                            // 忽略无法解析的情况
                        }
                    }
                }

                // 设置基础字段
                news.setTitle((String) sourceAsMap.get("title"));
                news.setSource((String) sourceAsMap.get("source"));
                news.setNewsType((String) sourceAsMap.get("newsType"));
                news.setContent((String) sourceAsMap.get("content"));

                // 处理 status 字段，兼容 Integer 和 String 类型
                Object statusObj = sourceAsMap.get("status");
                if (statusObj != null) {
                    if (statusObj instanceof Integer) {
                        news.setStatus((Integer) statusObj);
                    } else {
                        try {
                            news.setStatus(Integer.parseInt(statusObj.toString()));
                        } catch (NumberFormatException ignored) {
                        }
                    }
                }

                // 处理 createTime 字段（Elasticsearch 返回字符串格式）
                String createTimeStr = (String) sourceAsMap.get("createTime");
                if (createTimeStr != null) {
                    try {
                        news.setCreateTime(DateUtil.parse(createTimeStr)); // 使用工具类解析日期
                    } catch (Exception ignored) {
                        // 忽略解析失败的情况
                    }
                }

                // 替换高亮后的 title 和 content 内容（如果启用了高亮）
                Map<String, HighlightField> highlightFields = hit.getHighlightFields();
                if (highlightFields != null) {
                    if (highlightFields.containsKey("title")) {
                        news.setTitle(highlightFields.get("title").fragments()[0].string());
                    }
                    if (highlightFields.containsKey("content")) {
                        news.setContent(highlightFields.get("content").fragments()[0].string());
                    }
                }

                // 如果 newsId 和 title 存在，则调用点击方法增加热度
                if (news.getNewsId() != null && news.getTitle() != null) {
                    clickNews(news.getNewsId().toString(), news.getTitle());
                }

                // 返回封装好的 VO 对象
                return news;
            }
        } catch (IOException | ElasticsearchException e) {
            throw new RuntimeException("根据 newsId 查询新闻失败！", e);
        }

        // 查询失败或未命中，返回 null
        return null;
    }

    /**
     * 获取当前热搜新闻（前20）
     */
    @Override
    public List<HotNewsVO> getHotNews() {
//        RScoredSortedSet<String> hotNewsSet = redissonClient.getScoredSortedSet(HOT_NEWS_KEY);
        // 使用 StringCodec 明确指定以字符串方式处理
        RScoredSortedSet<String> hotNewsSet = redissonClient.getScoredSortedSet(HOT_NEWS_KEY, new StringCodec());

        // 取出前20名（按分数倒序）
        Collection<org.redisson.client.protocol.ScoredEntry<String>> top20Entries = hotNewsSet.entryRangeReversed(0, 19);

        List<HotNewsVO> result = new ArrayList<>();
        if (top20Entries != null) {
            for (org.redisson.client.protocol.ScoredEntry<String> entry : top20Entries) {
                String newsJson = entry.getValue();
                // 简化处理为字符串拼接："newsId,title"
                String[] parts = newsJson.split(",");
                if (parts.length >= 2) {
                    HotNewsVO vo = new HotNewsVO();
                    vo.setNewsId(parts[0]);
                    vo.setTitle(parts[1]);
                    result.add(vo);
                }
            }
        }

        return result;
    }


    /**
     * 将新闻数据保存到 Elasticsearch 中
     *
     * @param news 新闻数据封装对象（NewsVO）
     * @throws RuntimeException 保存失败时抛出异常
     */
    @Override
    public void saveNewsToEs(NewsVO news) throws RuntimeException {
        try {
            // 使用 XContentBuilder 构建要写入 Elasticsearch 的 JSON 数据
            XContentBuilder builder = XContentFactory.jsonBuilder()
                    .startObject()
                    .field("newsId", news.getNewsId())         // 新闻ID
                    .field("title", news.getTitle())           // 标题
                    .field("source", news.getSource())         // 来源
                    .field("newsType", news.getNewsType())     // 新闻类型
                    .field("content", news.getContent())       // 内容
                    .field("status", news.getStatus())         // 状态
                    .field("createTime", DateUtil.format(news.getCreateTime(), "yyyy-MM-dd HH:mm:ss")) // 创建时间，格式化为字符串
                    .endObject();

            // 构建索引请求对象，指定索引名和文档ID（必须是字符串）
            IndexRequest indexRequest = new IndexRequest(INDEX_NAME)
                    .id(news.getNewsId().toString())  // 设置文档ID为 newsId 字符串形式
                    .source(builder);                 // 设置文档内容

            // 执行写入操作，将新闻数据写入 Elasticsearch
            restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);

            // 如果 typeId == "6"，表示该新闻属于热搜类型，则同步添加到 Redis 的热搜榜单中
            if ("6".equals(news.getTypeId())) {
                addHotNewsToRedis(news.getNewsId().toString(), news.getTitle());
            }

        } catch (Exception e) {
            // 捕获异常并包装为运行时异常抛出
            throw new RuntimeException("写入ES失败：" + news.getNewsId(), e);
        }
    }

    /**
     * 从 Elasticsearch 中删除指定 ID 的新闻数据
     *
     * @param newsId 要删除的新闻ID
     */
    @Override
    public void deleteNewsFromEs(Long newsId) {
        // 参数校验：newsId 不可为空
        if (newsId == null) {
            return;
        }

        try {
            // 构建 DeleteRequest 请求，指定索引名和文档ID
            DeleteRequest deleteRequest = new DeleteRequest(INDEX_NAME, newsId.toString());

            // 执行删除操作
            DeleteResponse response = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);

            // 判断删除结果状态
            if (response.status() == RestStatus.NOT_FOUND) {
                // 文档不存在于 ES 中，可选处理
                System.out.println("要删除的新闻在ES中不存在：" + newsId);
            } else {
                // 删除成功
                System.out.println("新闻已从ES中删除：" + newsId);
            }

        } catch (IOException | ElasticsearchException e) {
            // 捕获异常并包装为运行时异常抛出
            throw new RuntimeException("从ES中删除新闻失败：" + newsId, e);
        }
    }

    /**
     * 模拟点击新闻，增加其热度（调用此方法测试热搜）
     */
    public void clickNews(String newsId, String title) {
        RScoredSortedSet<String> hotNewsSet = redissonClient.getScoredSortedSet(HOT_NEWS_KEY, new StringCodec());
        String newsKey = newsId + "," + title;

        // 每次点击 +1 分数
        hotNewsSet.addScore(newsKey, 1);

        // 控制最多保留20条
        if (hotNewsSet.size() > 20) {
            // 删除最低分的一个
            hotNewsSet.remove(hotNewsSet.first());
        }
    }

    /**
     * 将新闻加入热搜集合，若 typeId == 6，则设置一个初始分数
     */
    private void addHotNewsToRedis(String newsId, String title) {
        RScoredSortedSet<String> hotNewsSet = redissonClient.getScoredSortedSet(HOT_NEWS_KEY, new StringCodec());
        String newsKey = newsId + "," + title;

        // 设置初始分数
        double score = "6".equals(newsId) ? Double.MAX_VALUE : 50; // 这里应根据业务逻辑调整分数策略

        hotNewsSet.add(score, newsKey); // 添加或更新热度
    }

}